/*
 * pixiv_down - CLI-based downloading tool for https://www.pixiv.net.
 * Copyright (C) 2024  Mio
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * This file contains the necessary code to interact with plugins.
 * For documentation on defining your own plugin, see converter.h.
 */
module pd.converter;

import std.experimental.logger;

struct Converter
{
   @disable this(this); // no copy

   ~this()
   {
      if (m_handle)
         m_handle.dispose(m_handle);
   }

   bool appendFrame(string path)
   in
   {
      assert(m_handle);
      assert(m_handle.append_frame);
   }
   do
   {
      import std.string : toStringz;

      return m_handle.append_frame(m_handle, toStringz(path));
   }

   void setFrameDelay(ulong delay)
   in
   {
      assert(m_handle);
      assert(m_handle.set_frame_delay);
   }
   do
   {
      m_handle.set_frame_delay(m_handle, cast(uint) delay);
   }

   bool write(string path)
   in
   {
      assert(m_handle);
      assert(m_handle.write);
   }
   do
   {
      import std.string : toStringz;

      return m_handle.write(m_handle, toStringz(path));
   }

private:
   c_converter* m_handle;
}

class ConverterManager
{
   static ConverterManager load(string plugin)
   {
      import core.sys.posix.dlfcn;
      import std.string : toStringz, fromStringz;

      dlerror();

      void* library = findLibrary(plugin);

      if (!library)
      {
         errorf("Failed to load the %s plugin: %s", plugin, fromStringz(dlerror()));
         return null;
      }

      auto converter = new ConverterManager(library);

      void* sym = dlsym(library, "initialize_converter_library");
      if (!sym)
      {
         errorf("Failed to load the 'initialize_converter_library' symbol from %s", plugin);
         return null;
      }

      converter.initialize_library = cast(typeof(converter.initialize_library)) sym;

      sym = dlsym(library, "deinitialize_converter_library");
      if (!sym)
      {
         errorf("Failed to load the 'deinitialize_converter_library' symbol from %s", plugin);
         return null;
      }

      converter.deinitialize_library = cast(typeof(converter.deinitialize_library)) sym;

      return converter;
   }

   @disable this();

   bool initialize()
   {
      if (initialize_library)
         return initialize_library(&m_converterInfo);
      return false;
   }

   void deinitialize()
   {
      if (deinitialize_library)
         deinitialize_library(&m_converterInfo);
   }

   Converter createConverter(const(char)[] format) const
   in
   {
       assert(m_converterInfo.create_converter);
   }
   do
   {
      return Converter(m_converterInfo.create_converter(format.ptr));
   }

   string name() const
   {
      import std.exception : assumeUnique;
      import std.string : fromStringz;

      return assumeUnique(fromStringz(m_converterInfo.name));
   }

   uint apiVersion() const
   {
      return m_converterInfo.api_version;
   }

private:
   void* m_library = null;
   c_converter_info m_converterInfo;

   extern (C) bool function(c_converter_info*) initialize_library = null;
   extern (C) void function(c_converter_info*) deinitialize_library = null;

   this(void* library)
   {
      m_library = library;
   }

   ~this()
   {
      import core.sys.posix.dlfcn;

      if (m_library)
         dlclose(m_library);
   }
}

private:

void* findLibrary(const(char)[] plugin)
{
   import core.sys.posix.dlfcn;
   import std.string : toStringz, fromStringz;
   import mlib.directories : getProjectDirectories;

   const projectDirs = getProjectDirectories(null, "YumeNeru Software", "pixiv_down");
   const localPluginDir = projectDirs.dataDir ~ "/plugins/";

   version (OSX)
      const libraryName = "libpd_" ~ plugin ~ "_converter.dylib";
   else
      const libraryName = "libpd_" ~ plugin ~ "_converter.so";

   // Use $DATADIR plugins first.
   void* library = dlopen(toStringz(localPluginDir ~ libraryName), RTLD_LAZY | RTLD_LOCAL);
   if (library)
      return library;
   warningf("Could not find %s in %s", libraryName, localPluginDir);

   // Try /usr/local/lib/pd
   library = dlopen(toStringz("/usr/local/lib/pd/" ~ libraryName), RTLD_LAZY | RTLD_LOCAL);
   if (library)
      return library;
   warningf("Could not find %s in /usr/local/lib/pd", libraryName);

   // Try RPATH for macOS.
   version (OSX)
   {
      library = dlopen(toStringz("@rpath/" ~ libraryName), RTLD_LAZY | RTLD_LOCAL);
      if (library)
         return library;
      warningf("Could not find %s in @rpath", libraryName);
   }

   // Fallback to system library paths.
   library = dlopen(toStringz(libraryName), RTLD_LAZY | RTLD_LOCAL);
   if (!library)
      warningf("Could not find %s in system library paths.", libraryName);

   return library;
}

struct c_converter_info
{
   char* name;
   uint api_version;
   uint padding;

   extern (C) c_converter* function(const scope char* format) create_converter;
}

struct c_converter
{
   extern (C) bool function(c_converter*, const scope char* path) append_frame;
   extern (C) void function(c_converter*, uint delay) set_frame_delay;
   extern (C) bool function(c_converter*, const scope char* path) write;
   extern (C) void function(c_converter*) dispose;
}
